package com.shermanzhou.springbootflowable.demo;

import com.alibaba.fastjson.JSONObject;
import com.shermanzhou.springbootflowable.demo.flow2.EngineApprovalFlow;
import com.shermanzhou.springbootflowable.demo.flow2.EngineApprovalFlowNode2;
import com.shermanzhou.springbootflowable.demo.flow2.EngineApprovalFlowResult2;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.task.api.TaskInfo;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.flowable.variable.service.impl.persistence.entity.HistoricVariableInstanceEntityImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhouchaoqun02
 * @date 2020-06-14 19:55
 */
@SpringBootTest
public class ApprovalFlowTest5 {


    @Autowired
    @Qualifier("normalProcessEngine")
    private ProcessEngine normalEngine;

    @Autowired
    @Qualifier("simulateProcessEngine")
    private ProcessEngine simulateEngine;

    @Test
    public void finishedProcInstFlow() {
        String procInstId = "25001";
        HistoricProcessInstance historicProcessInstance = normalEngine.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(procInstId).singleResult();

        List<HistoricTaskInstance> hisTasks = normalEngine.getHistoryService().createHistoricTaskInstanceQuery().includeProcessVariables().includeTaskLocalVariables().processInstanceId(procInstId).list();

        List<HistoricActivityInstance> actHistoryList = normalEngine.getHistoryService().createHistoricActivityInstanceQuery().processInstanceId(procInstId).list();
        Process process = normalEngine.getRepositoryService().getBpmnModel(historicProcessInstance.getProcessDefinitionId()).getProcesses().get(0);

        long t1 = System.currentTimeMillis();
        Map<String, List<TaskInfo>> actIdAndHisTasksMap = hisTasks.stream().collect(Collectors.groupingBy(TaskInfo::getTaskDefinitionKey));
        Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap = actHistoryList.stream()
                .collect(Collectors.groupingBy(HistoricActivityInstance::getActivityId));


        EngineApprovalFlowResult2 flowResult = makeUpApprovalFlow(process, actIdAndActivitiesMap, actIdAndHisTasksMap);
        long t2 = System.currentTimeMillis();

        System.out.println(JSONObject.toJSONString(flowResult));


        System.out.println("execute cost: " + (t2 - t1));
    }


    private String nodeId(String actId, String flowId) {
        return String.format("%s@%s", actId, flowId);
    }

    private String flowId(String executionId) {
        return String.format("%s_%s", "flow", executionId);
    }


    private EngineApprovalFlowResult2 makeUpApprovalFlow(Process process,
                                    Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap,
                                    Map<String, List<TaskInfo>> actIdAndHisTasksMap) {
        FlowNode startNode = (FlowNode) process.getInitialFlowElement();
        HistoricActivityInstance startAct = actIdAndActivitiesMap.get(startNode.getId()).get(0);
        String flowId = flowId(startAct.getExecutionId());

        EngineApprovalFlowNode2 root = EngineApprovalFlowNode2.builder()
                .nodeId(nodeId(startAct.getActivityId(), flowId))
                .nodeName(startAct.getActivityName())
                .actId(startAct.getActivityId())
                .nodeType(startAct.getActivityType())
                .build();

        EngineApprovalFlow rootFlow = EngineApprovalFlow.builder().flowId(flowId).startNodeId(root.getNodeId()).build();
        rootFlow.addNode(root);

        Map<SubProcess, EngineApprovalFlowNode2> subProcessCollector = new HashMap<>();
        makeUpNextNodes(rootFlow, root, actIdAndActivitiesMap, process.getFlowElementMap(), actIdAndHisTasksMap, subProcessCollector);
        List<EngineApprovalFlow> subFlows = makeUpSubProcessApprovalFlow(rootFlow, subProcessCollector, actIdAndActivitiesMap, actIdAndHisTasksMap);

        EngineApprovalFlowResult2 flowResult = new EngineApprovalFlowResult2();
        flowResult.setRootFlowId(flowId);
        flowResult.addRootFlow(rootFlow);
        subFlows.forEach(flowResult::addSubFlow);

        return flowResult;
    }

    private List<EngineApprovalFlow> makeUpSubApprovalFlow(EngineApprovalFlowNode2 subProcessNode,
                                                           SubProcess subProcess,
                                                           Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap,
                                                           Map<String, List<TaskInfo>> actIdAndHisTasksMap) {
        List<EngineApprovalFlow> subFlows = new ArrayList<>();

        FlowNode startNode = (FlowNode) subProcess.getFlowElements().stream()
                .filter(flowElement -> flowElement instanceof StartEvent)
                .findFirst().orElseThrow(() -> new RuntimeException("subProcess start event not found"));
        List<HistoricActivityInstance> startActs = actIdAndActivitiesMap.get(startNode.getId());


        for (int i = 0; i < startActs.size(); i++) {
            HistoricActivityInstance startAct = startActs.get(i);
            String flowId = flowId(startAct.getExecutionId());
            EngineApprovalFlowNode2 root = EngineApprovalFlowNode2.builder()
                    .nodeId(nodeId(startAct.getActivityId(), flowId))
                    .nodeName(startAct.getActivityName() != null ? startAct.getActivityName() : startAct.getActivityType())
                    .actId(startAct.getActivityId())
                    .nodeType(startAct.getActivityType())
                    .collectionVariable(subProcessNode.getCollectionVariable())
                    .elementVariable(subProcessNode.getElementVariable())
                    .loopIdxAndElementValueMap(subProcessNode.getLoopIdxAndElementValueMap())
                    .executionId(startAct.getExecutionId())
                    .loopCounter(i) // startEvent 的创建顺序和 LoopCounter顺序一致
                    .elementValue(subProcessNode.getLoopIdxAndElementValueMap().get(String.valueOf(i)))
                    .build();
            EngineApprovalFlow subFLow = EngineApprovalFlow.builder().flowId(flowId).startNodeId(root.getNodeId()).build();
            subFLow.addNode(root);

            Map<SubProcess, EngineApprovalFlowNode2> subProcessCollector = new HashMap<>();
            makeUpNextNodes(subFLow, root, actIdAndActivitiesMap, subProcess.getFlowElementMap(), actIdAndHisTasksMap, subProcessCollector);
            subFlows.addAll(makeUpSubProcessApprovalFlow(subFLow, subProcessCollector, actIdAndActivitiesMap, actIdAndHisTasksMap));

            subFlows.add(subFLow);
        }

        return subFlows;
    }

    private List<EngineApprovalFlow> makeUpSubProcessApprovalFlow(EngineApprovalFlow parentFlow,
                                                                  Map<SubProcess, EngineApprovalFlowNode2> subProcessCollector,
                                                                  Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap,
                                                                  Map<String, List<TaskInfo>> actIdAndHisTasksMap) {
        List<EngineApprovalFlow> approvalFlows = new ArrayList<>();
        subProcessCollector.forEach((subProcess, subProcessNode) -> {
            List<EngineApprovalFlow> subFlows = makeUpSubApprovalFlow(subProcessNode, subProcess, actIdAndActivitiesMap, actIdAndHisTasksMap);
            completeSubFlowsMapForParentFlow(parentFlow, subProcessNode, subFlows);
            approvalFlows.addAll(subFlows);
        });
        return approvalFlows;
    }

    private void completeSubFlowsMapForParentFlow(EngineApprovalFlow parentFlow, EngineApprovalFlowNode2 subProcessNode, List<EngineApprovalFlow> subFlows) {
        Map<String, List<String>> subFlowsMap = new LinkedHashMap<>();
        for (EngineApprovalFlow subFlow : subFlows) {
            List<String> subFlowIds = subFlowsMap.computeIfAbsent(subProcessNode.getNodeId(), k -> new ArrayList<>());
            subFlowIds.add(subFlow.getFlowId());
        }
        parentFlow.setSubFlowsMap(subFlowsMap);
    }

    private void makeUpNextNodes(EngineApprovalFlow approvalFlow,
                                 EngineApprovalFlowNode2 curNode,
                                 Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap,
                                 Map<String, FlowElement> flowElementMap,
                                 Map<String, List<TaskInfo>> actIdAndHisTasksMap,
                                 Map<SubProcess, EngineApprovalFlowNode2> subProcessCollector) {
        FlowNode flowNode = (FlowNode) flowElementMap.get(curNode.getActId());
        List<FlowNode> nextNodes = getNextActiveNodes(flowNode, actIdAndActivitiesMap);
        if (nextNodes == null || nextNodes.size() == 0) {
            return;
        }

        for (FlowNode nextNode : nextNodes) {
            if (nextNode instanceof SubProcess) {
                SubProcess subProcess = (SubProcess) nextNode;
                Set<String> executionIds = getSubProcessExecutionIds(actIdAndActivitiesMap, subProcess);
                MultiInstanceLoopCharacteristics loopCharacteristics = subProcess.getLoopCharacteristics();
                Map<String, Object> loopIdxAndElementValueMap = getElementVariableValues("25001", loopCharacteristics, executionIds);
                EngineApprovalFlowNode2 node = EngineApprovalFlowNode2.builder()
                        .nodeId(nodeId(nextNode.getId(), approvalFlow.getFlowId()))
                        .nodeName(nextNode.getName())
                        .actId(nextNode.getId())
                        .nodeType("subProcess")
                        .collectionVariable(loopCharacteristics.getInputDataItem())
                        .elementVariable(loopCharacteristics.getElementVariable())
                        .loopIdxAndElementValueMap(loopIdxAndElementValueMap)
                        .build();
                subProcessCollector.put((SubProcess) nextNode, node);
                approvalFlow.addNode(node);
                approvalFlow.addOutGoing(curNode.getNodeId(), node.getNodeId());
            } else {
                TaskInfo taskInfo = findTaskForNode(nextNode.getId(), actIdAndHisTasksMap, curNode.getLoopCounter());
                HistoricActivityInstance act = getActivityInstanceForNode(nextNode.getId(), taskInfo, actIdAndActivitiesMap, curNode.getLoopCounter());

                EngineApprovalFlowNode2 node = EngineApprovalFlowNode2.builder()
                        .nodeId(nodeId(nextNode.getId(), approvalFlow.getFlowId()))
                        .nodeName(nextNode.getName())
                        .actId(nextNode.getId())
                        .nodeType(act.getActivityType())
                        .executionId(act.getExecutionId())
                        .loopCounter(curNode.getLoopCounter())
                        .taskInfo(taskInfo)
                        .build();
                approvalFlow.addNode(node);
                approvalFlow.addOutGoing(curNode.getNodeId(), node.getNodeId());
            }
        }
        approvalFlow.getNextNodes(curNode.getNodeId()).forEach(cn -> makeUpNextNodes(approvalFlow, cn, actIdAndActivitiesMap, flowElementMap, actIdAndHisTasksMap, subProcessCollector));
    }

    private HistoricActivityInstance getActivityInstanceForNode(String actId, TaskInfo taskInfo, Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap, Integer loopCounter) {
        HistoricActivityInstance act;
        List<HistoricActivityInstance> acts = actIdAndActivitiesMap.get(actId);
        if (acts.size() == 1) {
            act = acts.get(0);
        } else if (taskInfo == null) {
            if (loopCounter == null) {
                act = acts.get(0);
            } else {
                act = acts.get(loopCounter);
            }
        } else {
            act = findActivityInstanceByTaskInfo(taskInfo, acts);
        }
        return act;
    }

    private HistoricActivityInstance findActivityInstanceByTaskInfo(TaskInfo taskInfo, List<HistoricActivityInstance> acts) {
        return acts.stream()
                .filter(act -> act.getTaskId().equals(taskInfo.getId()))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("cannot match activity instance by task, taskId: " + taskInfo.getId()));
    }

    private Set<String> getSubProcessExecutionIds(Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap, SubProcess subProcess) {
        Set<String> executionIds = new HashSet<>();
        List<HistoricActivityInstance> acts = actIdAndActivitiesMap.get(subProcess.getId());
        if (acts != null && acts.size() != 0) {
            executionIds = acts.stream().map(HistoricActivityInstance::getExecutionId).collect(Collectors.toSet());
        }
        return executionIds;
    }

    private Map<String, Object> getElementVariableValues(String procInstId, MultiInstanceLoopCharacteristics loopCharacteristics, Set<String> executionIds) {
        Map<String, Object> loopIdxAndElementValueMap = new HashMap<>();

        List<HistoricVariableInstance> elementValues = normalEngine.getHistoryService().createHistoricVariableInstanceQuery()
                .processInstanceId(procInstId)
                .executionIds(executionIds)
                .list();
        Map<String, List<HistoricVariableInstance>> exeIdAndVarsMap = elementValues.stream().collect(Collectors.groupingBy(v -> ((HistoricVariableInstanceEntityImpl) v).getExecutionId()));
        exeIdAndVarsMap.forEach((exeId, vars) -> {
            String loopIdx = null;
            Object elementValue = null;
            for (HistoricVariableInstance var : vars) {
                if ("loopCounter".equals(var.getVariableName())) {
                    loopIdx = String.valueOf(var.getValue());
                }
                if (loopCharacteristics.getElementVariable().equals(var.getVariableName())) {
                    elementValue = var.getValue();
                }
            }
            loopIdxAndElementValueMap.put(loopIdx, elementValue);
        });
        return loopIdxAndElementValueMap;
    }

    private TaskInfo findTaskForNode(String actId, Map<String, List<TaskInfo>> actIdAndHisTasksMap, Integer loopCounter) {
        List<TaskInfo> taskInfos = actIdAndHisTasksMap.get(actId);
        if (taskInfos == null || taskInfos.size() == 0) {
            return null;
        }

        // 单实例任务
        if (loopCounter == null) {
            return taskInfos.get(0);
        }

        // 多实例，但是仅有一个实例
        if (taskInfos.size() == 1) {
            return taskInfos.get(0);
        }

        TaskInfo ret = null;
        for (TaskInfo task : taskInfos) {
            HistoricVariableInstance loopCounterVar = normalEngine.getHistoryService().createHistoricVariableInstanceQuery()
                    .executionId(task.getExecutionId()).variableName("loopCounter").singleResult();
            if (loopCounterVar != null && Objects.equals(loopCounterVar.getValue(), loopCounter)) {
                ret = task;
            }
        }
        if (ret != null) {
            return ret;
        }

        throw new RuntimeException("except one task for activity instance, but not found");
    }

    private List<FlowNode> getNextActiveNodes(FlowNode flowNode, Map<String, List<HistoricActivityInstance>> actIdAndActivitiesMap) {
        List<FlowNode> activeNodes = new ArrayList<>();
        for (SequenceFlow out : flowNode.getOutgoingFlows()) {
            FlowNode targetFlowElement = (FlowNode) out.getTargetFlowElement();
            if (actIdAndActivitiesMap.containsKey(targetFlowElement.getId())) {
                activeNodes.add(targetFlowElement);
            }
        }

        List<FlowNode> result = new ArrayList<>();
        for (FlowNode node : activeNodes) {
            if (node instanceof Gateway) {
                List<FlowNode> nodesComeFromGateway = getNextActiveNodes(node, actIdAndActivitiesMap);
                result.addAll(nodesComeFromGateway);
            } else {
                result.add(node);
            }
        }
        return result;
    }

}
